#!/usr/bin/env python3
# -*- coding: utf-8 -*-

""" This module processes the gray and bin-value images."""

import math

import numpy as np
# import matplotlib.pylab as plt
from PIL import Image
from sklearn.cluster import KMeans



def window(im, x, y, miss=None):
    miss = miss or (1 if im.mode == '1' else 255)
    pixels = im.load()
    nw = pixels[x - 1, y - 1] if x > 0 and y > 0 else miss
    n = pixels[x, y - 1] if y > 0 else miss
    ne = pixels[x + 1, y - 1] if x < im.width - 1 and y > 0 else miss
    w = pixels[x - 1, y] if x > 0 else miss
    e = pixels[x + 1, y] if x < im.width - 1 else miss
    sw = pixels[x - 1, y + 1] if x > 0 and y < im.height - 1 else miss
    s = pixels[x, y + 1] if y < im.height - 1 else miss
    se = pixels[x + 1, y + 1] if x < im.width - 1 and y < im.height - 1 else miss
    return nw, n, ne, w, e, sw, s, se


def neighbors(im, x, y, bgcolor=None):
    bgcolor = bgcolor or (1 if im.mode == '1' else 255)
    return sum(1 if p == bgcolor else 0 for p in window(im, x, y, bgcolor))


def wizened(im, x, y, bgcolor=None):
    bgcolor = bgcolor or (1 if im.mode == '1' else 255)
    nw, n, ne, w, e, sw, s, se = window(im, x, y)
    _wizened = 0
    _wizened += 1 if nw == bgcolor and se == bgcolor else 0
    _wizened += 1 if n == bgcolor and s == bgcolor else 0
    _wizened += 1 if ne == bgcolor and sw == bgcolor else 0
    _wizened += 1 if w == bgcolor and e == bgcolor else 0
    return _wizened


def wipe(im, n=4, bgcolor=None):
    bgcolor = bgcolor or (1 if im.mode == '1' else 255)
    coords = [(x, y) for x in range(im.width) for y in range(im.height)]
    pixels = im.load()
    for _ in range(n):
        for x, y in coords:
            if wizened(im, x, y) > 0:
                pixels[x, y] = bgcolor
    return im


def distribute(im):
    assert im.mode == 'L'
    colors = im.getcolors(im.width * im.height)
    dist = [0] * 256
    for n, c in colors:
        dist[c] = n
    return dist


def otsu(colors):
    """ OSTU Algorithm used for bin-value on gray images.
        http://blog.csdn.net/tobacco5648/article/details/8787927
    """
    _max = 0
    _best = 0
    size = len(colors)
    w = [0] * size
    px = [0] * size
    w [0] = colors[0]
    for m in range(1, size):
        w[m] = w[m - 1] + colors[m]
        px[m] = px[m - 1] + colors[m] * m
    for th in range(size):
        w1 = w[th]
        w2 = w[size - 1] - w1
        if w1 * w2 == 0:
            continue
        u1 = px[th] / w1
        u2 = (px[size - 1] - px[th]) / w2
        v = w1 * w2 * (u1 - u2) ** 2
        if v > _max:
            _max = v
            _best = th
    return _best


def bin_value(im):
    gray = im.convert('L')
    threshold = otsu(distribute(gray))
    bitmap = [0 if x < threshold else 1 for x in range(256)]
    return gray.point(bitmap, '1')


def project(im, axis='x', bounds=None, bgcolor=None):
    left, top, right, bottom = bounds or (0, 0, im.width, im.height)
    bgcolor = bgcolor or (1 if im.mode == '1' else 255)
    if axis.lower() == 'x':
        outer = left, right
        inner = top, bottom
    elif axis.lower() == 'y':
        outer = top, bottom
        inner = left, right
    else:
        raise KeyError('Axis must be of `x` or `y`')
    pixels = im.load()
    stat = []
    for i in range(*outer):
        ctr = 0
        for j in range(*inner):
            x, y = (i, j) if axis.lower() == 'x' else (j, i)
            if pixels[x, y] != bgcolor:
                ctr += 1
        stat += [ctr]
    return stat


def regions(stat):
    sections = []
    end = len(stat)
    left, right = -1, end
    for i in range(end):
        if left < 0 and stat[i] != 0:
            left = i
        if left >= 0 and stat[i] == 0:
            right = i
            sections += [(left, right)]
            left = -1
    if left >= 0:
        sections += [(left, right)]
    return sections


def pickout(im, bgcolor=None):
    for l, r in regions(project(im, 'x', None, bgcolor)):
        for t, b in regions(project(im, 'y', (l, 0, r, im.height), bgcolor)):
            for ll, rr in regions(project(im, 'x', (l, t, r, b), bgcolor)):
                yield ll + l, t, rr + l, b


def rotate(im, angle):
    """ Rotate image to ``angle`` degrees counter clockwise around,
        auto expand to hold all the pixels in the raw image.
    """
    rot = im.convert('RGBA').rotate(angle, expand=True)
    alpha = Image.new('RGBA', rot.size, (255,)*4)
    out = Image.composite(rot, alpha, rot)
    return out.convert('L').point([0] + [1] * 255, '1')


def calipers(im, limit=30):
    """ Rotating Calipers Algorithm."""
    width = im.width
    angle = 0
    for ang in range(-limit, limit + 1):
        tmp = rotate(im, ang)
        left, right = regions(project(tmp))[0]
        _width = right - left
        if _width < width:
            width = _width
            angle = ang
    return angle


def corrode(im, bgcolor=None):
    pass


def generalize(im, size=(16, 16), bgcolor=None):
    bgcolor = bgcolor or (1 if im.mode == '1' else 255)
    tmp = rotate(im, calipers(im))
    wipe(tmp, 2)
    border = [*pickout(tmp, bgcolor)][0]
    return tmp.crop(border).resize(size)


if __name__ == '__main__':
    bitmap = [0 if x < 160 else 1 for x in range(256)]
    with Image.open('./samples/2.png') as img:
        im = bin_value(img)
    wipe(im)
    # im.show()
    pics = [im.crop(borders) for borders in pickout(im)]
    pics = [p for p in pics if p.width * p.height > 80]
    pics = [generalize(p) for p in pics]
